using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;

namespace Lightbug.CharacterControllerPro.Demo
{

    [AddComponentMenu("Character Controller Pro/Demo/Camera/Camera 3D")]
    [DefaultExecutionOrder(ExecutionOrder.CharacterGraphicsOrder + 100)]  // <--- Do your job after everything else
    public class Camera3D : MonoBehaviour
    {
        [Header("Inputs")]

        [SerializeField]
        InputHandlerSettings inputHandlerSettings = new InputHandlerSettings();

        [SerializeField]
        string axes = "Camera";

        [SerializeField]
        string zoomAxis = "Camera Zoom";

        [Header("Target")]


        [Tooltip("Select the graphics root object as your target, the one containing all the meshes, sprites, animated models, etc. \n\nImportant: This will be the considered as the actual target (visual element).")]
        [SerializeField]
        Transform targetTransform = null;

        [SerializeField]
        Vector3 offsetFromHead = Vector3.zero;

        [Tooltip("The interpolation speed used when the height of the character changes.")]
        [SerializeField]
        float heightLerpSpeed = 10f;

        [Header("View")]

        public CameraMode cameraMode = CameraMode.ThirdPerson;

        [Header("First Person")]

        public bool hideBody = true;

        [SerializeField]
        GameObject bodyObject = null;

        [Header("Yaw")]

        public bool updateYaw = true;

        public float yawSpeed = 180f;


        [Header("Pitch")]

        public bool updatePitch = true;

        [SerializeField]
        float initialPitch = 45f;

        public float pitchSpeed = 180f;

        [Range(1f, 85f)]
        public float maxPitchAngle = 80f;

        [Range(1f, 85f)]
        public float minPitchAngle = 80f;


        [Header("Roll")]
        public bool updateRoll = false;


        [Header("Zoom (Third person)")]

        public bool updateZoom = true;

        [Min(0f)]
        [SerializeField]
        float distanceToTarget = 5f;

        [Min(0f)]
        public float zoomInOutSpeed = 40f;

        [Min(0f)]
        public float zoomInOutLerpSpeed = 5f;

        [Min(0f)]
        public float minZoom = 2f;

        [Min(0.001f)]
        public float maxZoom = 12f;


        [Header("Collision")]

        public bool collisionDetection = true;
        public bool collisionAffectsZoom = false;
        public float detectionRadius = 0.5f;
        public LayerMask layerMask = 0;
        public bool considerKinematicRigidbodies = true;
        public bool considerDynamicRigidbodies = true;

        // ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
        // ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
        // ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────


        CharacterActor characterActor = null;
        Rigidbody characterRigidbody = null;

        float currentDistanceToTarget;
        float smoothedDistanceToTarget;

        float deltaYaw = 0f;
        float deltaPitch = 0f;
        float deltaZoom = 0f;

        Vector3 lerpedCharacterUp = Vector3.up;       

        Transform viewReference = null;
        Renderer[] bodyRenderers = null;
        RaycastHit[] hitsBuffer = new RaycastHit[10];
        RaycastHit[] validHits = new RaycastHit[10];
        Vector3 characterPosition = default(Vector3);
        float lerpedHeight;

        public enum CameraMode
        {
            FirstPerson,
            ThirdPerson,
        }


        public void ToggleCameraMode()
        {
            cameraMode = cameraMode == CameraMode.FirstPerson ? CameraMode.ThirdPerson : CameraMode.FirstPerson;
        }

        
        
        void OnValidate()
        {
            initialPitch = Mathf.Clamp(initialPitch, -minPitchAngle, maxPitchAngle);
        }

        void Awake()
        {
            Initialize(targetTransform);
        }

        public bool Initialize(Transform targetTransform)
        {
            if (targetTransform == null)
                return false;

            characterActor = targetTransform.GetComponentInBranch<CharacterActor>();

            if (characterActor == null || !characterActor.isActiveAndEnabled)
            {
                Debug.Log("The character actor component is null, or it is not active/enabled.");
                return false;
            }

            characterRigidbody = characterActor.GetComponent<Rigidbody>();

            inputHandlerSettings.Initialize(gameObject);

            GameObject referenceObject = new GameObject("Camera reference");
            viewReference = referenceObject.transform;

            if (bodyObject != null)
                bodyRenderers = bodyObject.GetComponentsInChildren<Renderer>();

            return true;
        }

        void OnEnable()
        {
            if (characterActor == null)
                return;

            characterActor.OnTeleport += OnTeleport;
        }

        void OnDisable()
        {
            if (characterActor == null)
                return;

            characterActor.OnTeleport -= OnTeleport;
        }

       

        void Start()
        {

            characterPosition = targetTransform.position;

            previousLerpedCharacterUp = targetTransform.up;
            lerpedCharacterUp = previousLerpedCharacterUp;


            currentDistanceToTarget = distanceToTarget;
            smoothedDistanceToTarget = currentDistanceToTarget;

            viewReference.rotation = targetTransform.rotation;
            viewReference.Rotate(Vector3.right, initialPitch);

            lerpedHeight = characterActor.BodySize.y;
        }


        void Update()
        {
            if (targetTransform == null)
            {
                this.enabled = false;
                return;
            }

            Vector2 cameraAxes = inputHandlerSettings.InputHandler.GetVector2(axes);

            if (updatePitch)
                deltaPitch = -cameraAxes.y;

            if (updateYaw)
                deltaYaw = cameraAxes.x;

            if (updateZoom)
                deltaZoom = -inputHandlerSettings.InputHandler.GetFloat(zoomAxis);

            // An input axis value (e.g. mouse x) usually gets accumulated over time. So, the higher the frame rate the smaller the value returned.
            // In order to prevent inconsistencies due to frame rate changes, the camera movement uses a fixed delta time, instead of the old regular
            // delta time.
            float dt = Time.fixedDeltaTime;

            UpdateCamera(dt);
        }



        


        void OnTeleport(Vector3 position, Quaternion rotation)
        {
            viewReference.rotation = rotation;
            transform.rotation = viewReference.rotation;

            lerpedCharacterUp = characterActor.Up;
            previousLerpedCharacterUp = lerpedCharacterUp;

        }


        Vector3 previousLerpedCharacterUp = Vector3.up;

        void HandleBodyVisibility()
        {
            if (cameraMode == CameraMode.FirstPerson)
            {
                if (bodyRenderers != null)
                    for (int i = 0; i < bodyRenderers.Length; i++)
                    {
                        if (bodyRenderers[i].GetType().IsSubclassOf(typeof(SkinnedMeshRenderer)))
                        {
                            SkinnedMeshRenderer skinnedMeshRenderer = (SkinnedMeshRenderer)bodyRenderers[i];
                            if (skinnedMeshRenderer != null)
                                skinnedMeshRenderer.forceRenderingOff = hideBody;
                        }
                        else
                        {
                            bodyRenderers[i].enabled = !hideBody;
                        }
                    }

            }
            else
            {
                if (bodyRenderers != null)
                    for (int i = 0; i < bodyRenderers.Length; i++)
                    {
                        if (bodyRenderers[i] == null)
                            continue;

                        if (bodyRenderers[i].GetType().IsSubclassOf(typeof(SkinnedMeshRenderer)))
                        {
                            SkinnedMeshRenderer skinnedMeshRenderer = (SkinnedMeshRenderer)bodyRenderers[i];
                            if (skinnedMeshRenderer != null)
                                skinnedMeshRenderer.forceRenderingOff = false;
                        }
                        else
                        {
                            bodyRenderers[i].enabled = true;
                        }


                    }
            }
        }

        

        void UpdateCamera(float dt)
        {
            // Body visibility ---------------------------------------------------------------------
            HandleBodyVisibility();

            // Rotation -----------------------------------------------------------------------------------------
            lerpedCharacterUp = targetTransform.up;

            // Rotate the reference based on the lerped character up vector 
            Quaternion deltaRotation = Quaternion.FromToRotation(previousLerpedCharacterUp, lerpedCharacterUp);
            previousLerpedCharacterUp = lerpedCharacterUp;

            viewReference.rotation = deltaRotation * viewReference.rotation;



            // Yaw rotation -----------------------------------------------------------------------------------------        
            viewReference.Rotate(lerpedCharacterUp, deltaYaw * yawSpeed * dt, Space.World);

            // Pitch rotation -----------------------------------------------------------------------------------------            

            float angleToUp = Vector3.Angle(viewReference.forward, lerpedCharacterUp);


            float minPitch = -angleToUp + (90f - minPitchAngle);
            float maxPitch = 180f - angleToUp - (90f - maxPitchAngle);

            float pitchAngle = Mathf.Clamp(deltaPitch * pitchSpeed * dt, minPitch, maxPitch);
            viewReference.Rotate(Vector3.right, pitchAngle);

            // Roll rotation -----------------------------------------------------------------------------------------    
            if (updateRoll)
            {
                viewReference.up = lerpedCharacterUp;//Quaternion.FromToRotation( viewReference.up , lerpedCharacterUp ) * viewReference.up;
            }

            // Position of the target -----------------------------------------------------------------------
            characterPosition = targetTransform.position;

            lerpedHeight = Mathf.Lerp(lerpedHeight, characterActor.BodySize.y, heightLerpSpeed * dt);
            Vector3 targetPosition = characterPosition + targetTransform.up * lerpedHeight + targetTransform.TransformDirection(offsetFromHead);
            viewReference.position = targetPosition;

            Vector3 finalPosition = viewReference.position;

            // ------------------------------------------------------------------------------------------------------
            if (cameraMode == CameraMode.ThirdPerson)
            {
                currentDistanceToTarget += deltaZoom * zoomInOutSpeed * dt;
                currentDistanceToTarget = Mathf.Clamp(currentDistanceToTarget, minZoom, maxZoom);

                smoothedDistanceToTarget = Mathf.Lerp(smoothedDistanceToTarget, currentDistanceToTarget, zoomInOutLerpSpeed * dt);
                Vector3 displacement = -viewReference.forward * smoothedDistanceToTarget;

                if (collisionDetection)
                {
                    bool hit = DetectCollisions(ref displacement, targetPosition);

                    if (collisionAffectsZoom && hit)
                    {
                        currentDistanceToTarget = smoothedDistanceToTarget = displacement.magnitude;
                    }
                }

                finalPosition = targetPosition + displacement;
            }


            transform.position = finalPosition;
            transform.rotation = viewReference.rotation;

        }


        

        bool DetectCollisions(ref Vector3 displacement, Vector3 lookAtPosition)
        {
            int hits = Physics.SphereCastNonAlloc(
                lookAtPosition,
                detectionRadius,
                Vector3.Normalize(displacement),
                hitsBuffer,
                currentDistanceToTarget,
                layerMask,
                QueryTriggerInteraction.Ignore
            );

            // Order the results
            int validHitsNumber = 0;
            for (int i = 0; i < hits; i++)
            {
                RaycastHit hitBuffer = hitsBuffer[i];

                Rigidbody detectedRigidbody = hitBuffer.collider.attachedRigidbody;

                // Filter the results ---------------------------
                if (hitBuffer.distance == 0)
                    continue;

                if (detectedRigidbody != null)
                {
                    if (considerKinematicRigidbodies && !detectedRigidbody.isKinematic)
                        continue;

                    if (considerDynamicRigidbodies && detectedRigidbody.isKinematic)
                        continue;

                    if (detectedRigidbody == characterRigidbody)
                        continue;
                }

                //----------------------------------------------            
                validHits[validHitsNumber] = hitBuffer;
                validHitsNumber++;
            }

            if (validHitsNumber == 0)
                return false;


            float distance = Mathf.Infinity;
            for (int i = 0; i < validHitsNumber; i++)
            {
                RaycastHit hitBuffer = validHits[i];

                if (hitBuffer.distance < distance)
                    distance = hitBuffer.distance;
            }

            displacement = CustomUtilities.Multiply(Vector3.Normalize(displacement), distance);


            return true;
        }

        
    }

}
